package org.marsdonne.auth;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.KeyUse;
import com.nimbusds.jose.jwk.RSAKey;
import org.marsdonne.archives.Client;
import org.marsdonne.archives.Platform;
import org.marsdonne.audits.AuthAuditable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import javax.annotation.Resource;
import javax.sql.DataSource;
import javax.transaction.Transactional;
import java.security.KeyPair;
import java.security.interfaces.RSAPublicKey;
import java.util.*;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
    private static final String KEY_STORE_FILE = "keys.p12";
    private static final String KEY_STORE_PASSWORD = "marsdonne";
    private static final String KEY_ALIAS = "marsdonne.org";
    private static final String JWK_KID = "my-kid";

    @Resource
    private AuthenticationManager authenticationManager;
    @Resource
    private DataSource dataSource;
    @Resource
    private EndPointUserRepos endPointUserRepos;
    @Resource
    private AuthAuditable authAuditServ;
    @Resource
    private ObjectMapper objectMapper;

    @Bean
    public ClientDetailsService clientDetailsService() {
        return new JdbcClientDetailsService(dataSource);
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsService() {
            @Override
            @Transactional
            public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
                Optional<EndPointUser> optionalEndPointUser = endPointUserRepos.findByIdentifier(username);

                if (optionalEndPointUser.isPresent()) {
                    try {
                        EndPointUser endPointUser = optionalEndPointUser.get();
                        String realName = "";
                        int residentRegionCode = 0;
                        Long orgId = null;
                        String orgName = null;
                        String orgLicenseCode = null;
                        int orgRegionCode = 0;

                        UnionUser unionUser = endPointUser.getUnionUser();
                        if (unionUser != null) {
                            realName = unionUser.getRealName();
                            if (unionUser.getOrganizationId() != null)
                                orgId = unionUser.getOrganizationId();
                            if (unionUser.getOrganizationName() != null)
                                orgName = unionUser.getOrganizationName();
                            if (unionUser.getOrganizationLicenseCode() != null)
                                orgLicenseCode = unionUser.getOrganizationLicenseCode();
                        }

                        Set<SimpleGrantedAuthority> authorities = new HashSet<>();

                        Map<String, Object> additions = new LinkedHashMap(){{
                            put(Client.class.getSimpleName(), endPointUser.getClient());
                            put(Platform.class.getSimpleName(), endPointUser.getPlatform());
                        }};
                        authAuditServ.authenticate(endPointUser.getIdentifier(), objectMapper.convertValue(additions, JsonNode.class));

                        return new UserSecret(
                                endPointUser.getId(), endPointUser.getIdentifier(), endPointUser.getSecretCode(),
                                authorities, realName, residentRegionCode, endPointUser.getClient(),
                                orgId, orgLicenseCode, orgName, orgRegionCode,
                                false);
                    } catch (Exception ex) {
                        ex.printStackTrace();
                        throw new RuntimeException(ex.getMessage());
                    }
                }
                throw new UsernameNotFoundException("Not found user");
            }
        };
    }

    @Bean
    public KeyPair keyPair() {
        KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource(KEY_STORE_FILE), KEY_STORE_PASSWORD.toCharArray());
        return keyStoreKeyFactory.getKeyPair(KEY_ALIAS);
    }

    @Bean
    public JWKSet jwkSet() {
        return new JWKSet(
                new RSAKey.Builder((RSAPublicKey) keyPair().getPublic())
                        .algorithm(JWSAlgorithm.RS256)
                        .keyUse(KeyUse.SIGNATURE)
                        .keyID(JWK_KID)
                        .build());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        return new JwsAccessTokenConverter(
                new LinkedHashMap<String, String>() {{
                    put("kid", JWK_KID);
                }}, keyPair());
    }

    @Bean
    public TokenStore tokenStore(JwtAccessTokenConverter accessTokenConverter) {
        return new JwtTokenStore(accessTokenConverter);
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(clientDetailsService());
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .accessTokenConverter(accessTokenConverter())
                .reuseRefreshTokens(false)
                .userDetailsService(userDetailsService());
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security.tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()");
    }

}
